package org.springframework.security.samples.lock;


import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.samples.other.TimeUtil;
import org.springframework.stereotype.Service;

/**
 * @author Thomas Chen
 */
@Service
@Slf4j
public class LockService {


    private final RedisTemplate redisTemplate;

    //RedisTemplate can't delete the key;
    private final StringRedisTemplate stringRedisTemplate;

    /**
     * timeoutMsecs 锁等待超时，防止线程饥饿，永远没有入锁执行代码的机会
     */
    public final static int LOCK_TIMEOUT = 60 * 1000; // 1 min

    /**
     * expireMsecs 锁持有超时，防止线程在入锁以后，无限的执行下去，让锁无法释放
     */
    //public final static int expireMsecs = 60 * 1000; // 1 min

    public final static String LOCK_PREFIX = "REDIS_LOCK_";
    public final static long LOCK_EXPIRE = 60 * 1000; // 1 min

    @Autowired
    public LockService(RedisTemplate redisTemplate, StringRedisTemplate stringRedisTemplate) {
        this.redisTemplate = redisTemplate;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * Acquire lock.
     *
     * @return true if lock is acquired, false acquire timeouted
     * @throws InterruptedException in case of thread interruption
     */
/*    public synchronized boolean acquireLock(String lockKey) throws InterruptedException {
        int timeout = timeoutMsecs;
        while (timeout >= 0) {
            long expires = System.currentTimeMillis() + expireMsecs + 1;
            // 锁到期时间
            String expiresStr = String.valueOf(expires);
            // setnx命令就是如果key没有value则给key设置值value并返回1否则返回0
            // 全称：SET if Not eXists
            //if no one is owning the lock
            if (redisTemplate.setnx(lockKey, expiresStr) == 1) {
                // lock acquired
                //locked = true;
                return true;
            } else { //if some one is owning the lock
                // 获取redis中这个key的值即设置的过期时间戳
                String currentValueStr = redisTemplate.get(lockKey);
                //判断是否为空，不为空的情况下，如果被其他线程设置了值，则第二个条件判断是过不去的
                //在不为空的条件下，判断之前线程设置的加锁时间是否过期，如果没有过期则表示该线程还没有释放该锁，所以该线程获取不到这个锁
                //if some one is owning the lock & the lock is expired
                if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
                    // lock is expired
                    // 获取上一个锁到期时间，并设置现在的锁到期时间
                    // 只有一个线程才能获得上一个线程设置的时间，因为jedis.getSet是同步的
                    String oldValueStr = redisTemplate.getSet(lockKey, expiresStr);
                    // 如果这个时候多个线程恰好都到了这里，但是只有一个线程获得了之前锁的时间戳，所以只有这个线程才能获得本次的锁
                    if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
                        // lock acquired
                        //locked = true;
                        return true;
                    }
                }
            }
            timeout -= 1000;
            Thread.sleep(1000);
        }
        return false;
    }*/


    /**
     * Redis Distributed Lock
     *
     * @param key
     * @return
     */
    public boolean lock(String key) {
        String lockKeyName = LOCK_PREFIX + key;
        log.info("Lock key name: " + lockKeyName);
        // lambda
        return (Boolean) redisTemplate.execute(new RedisCallback<Object>() {
            @Override
            public Object doInRedis(RedisConnection redisConnection) throws DataAccessException {
                int timeout = LOCK_TIMEOUT;
                byte[] byteLockKeyName = lockKeyName.getBytes();
                while (timeout >= 0) {
                    log.debug("timeout: " + timeout);
                    //the lock value is the lock expired time
                    //System.currentTimeMillis() is na seconds
                    long expireAt = System.currentTimeMillis() + LOCK_EXPIRE + 1;
                    //if no one is owning the lock
                    Boolean acquire = redisConnection.setNX(byteLockKeyName, String.valueOf(expireAt).getBytes());
                    if (acquire) {
                        log.info("Got the lock as no one is using the lock.");
                        return true;
                    } else {
                        String currentLockValue = new String(redisConnection.get(byteLockKeyName));
                        log.debug("currentLockValue: " + currentLockValue);
                        if (StringUtils.isNotBlank(currentLockValue)) {
                            long currentLockExpireTime = Long.parseLong(currentLockValue);
                            log.debug("currentLockExpireTime: {} , {}", currentLockExpireTime, TimeUtil.formatMilliseconds(currentLockExpireTime));
                            log.debug("currentTimeMillis: {} , {}", System.currentTimeMillis(), TimeUtil.formatMilliseconds(System.currentTimeMillis()));
                            log.info("{} {} seconds.",
                                    (System.currentTimeMillis() - currentLockExpireTime > 0 ? "The Lock expired " : "The Lock haven't expired yet, will expire after "),
                                    Math.abs(System.currentTimeMillis() - currentLockExpireTime) / 1000);
                            if (currentLockExpireTime < System.currentTimeMillis()) { // if lock expired
                                long nextLockExpireTime = System.currentTimeMillis() + LOCK_EXPIRE + 1;
                                String expireTimeOfLatestUsingLock = new String(
                                        redisConnection.getSet(byteLockKeyName, String.valueOf(nextLockExpireTime).getBytes()));
                                log.debug("expireTimeOfLatestUsingLock: {} , {}", expireTimeOfLatestUsingLock,
                                        TimeUtil.formatMilliseconds(Long.parseLong(expireTimeOfLatestUsingLock)));
                                log.debug("nextLockExpireTime: {} , {}", nextLockExpireTime, TimeUtil.formatMilliseconds(nextLockExpireTime));
                                // make sure the lock is really expired and no other user/object is using
                                // if this thread obtained the timestamp of last using lock, which means no one is using this lock, then this thread can obtain the lock
                                //or it means another thread got the lock before this one
                                if (expireTimeOfLatestUsingLock.equals(currentLockValue)) {
                                    log.info("Got the lock as last lock is expired.");
                                    return true;
                                } else {
                                    log.info("Some thread got the lock before this one can get, its expireTime: {}", expireTimeOfLatestUsingLock);
                                }
                            }
                        }
                    }
                    timeout -= 1000;
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        log.error("", e);
                    }
                }
                return false;
            }
        });
    }


    public void releaseLock(String key) {
        String lockKeyName = LOCK_PREFIX + key;
        //redisTemplate.delete(lockKeyName);
        stringRedisTemplate.delete(lockKeyName);
        log.info("Deleted the lock: " + lockKeyName);
    }
}
